#!/usr/bin/python
#
# $Id: altai2ipa.py 94 2007-12-11 03:56:20Z altaireal $
#
# Converts REAL Altaic Phonetic Transcription to Unicode IPA symbols

import codecs
import sys
import re
from optparse import OptionParser, TitledHelpFormatter 
import time
import datetime
import os

version = "%.2f" % (float(re.compile("(\d+)").search("$Revision: 94 $").group(1)) /100)
date = "$Date: 2007-12-11 03:56:20 +0000 (Tue, 11 Dec 2007) $"[7:-2]

#
# Error
#
class Error(Exception):
	"""Base classe for exceptions in this module."""
	pass

class DictionaryError(Error):

	def __init__(self, lineno, entry, message):
		self.lineno = lineno
		self.entry = entry
		self.message = message

class InputError(Error):

	def __init__(self, expression, errors):
		self.expression = expression
		self.errors = errors


#
# Dictionary
#
class Dictionary :
	"""
	A dictionary file has two columns separated by one tab. First
	column contains the one (source forms), sencond column contains 
	the other (target forms).
	
	For example, (watch out! there should be a tab instead of spaces)

    I     Je
	am    suis
	free  libre
	"""
	#
	dic = {}
	env = {}
	
	def __init__ (self, dicfile, mode='default'):
		self.mode = mode
		self.read_dic(dicfile)
		self.filename = dicfile.name

	def read_dic (self, dicfile) :
		lineno = 0
		for line in dicfile:
			lineno += 1

			# process comments
			if line[0] == "#":
				m = re.compile("^#+\s+(opener|closer)\s+(.+)").match(line)
				if m: 
					self.env[m.group(1)] = m.group(2)
				continue
			elif re.compile("\s+").match(line):
				continue
			elif line[0:2] == "\#":
				line = line[1:]

			# split entry line
			try:
				(before, after) = line.strip().split("\t")[0:2]
			except ValueError, e:
				DictionaryError(lineno, source, "Illegal entry")
				sys.exit(1)

			# mode
			if self.mode == "default" :
				source = before.strip()
				target =  after.strip()
			elif self.mode == "reverse" :
				source = after.strip()
				target = before.strip()
			else:
				raise DictionaryError("", "", "Unknown mode")

			# put into hash
			if self.dic.has_key(source):
				raise DictionaryError(lineno, line, "Entry already exists.")
			else:
				self.dic[source] = target
	
	def print_dic (self) :
		for before in self.dic.keys():
			print before, self.dic[before]

	def getenv(self, var):
		if self.env.has_key(var):
			return self.env[var]
		else:
			return None	
	
	def keys(self):
		return self.dic.keys()
	
	def has_key(self, element):
		return self.dic.has_key(element)

	def __getitem__(self, key):
		return self.dic[key]

#
# Transliterator
#
class Transliterator :
	"""Tranlisterator
	translates input string into another form. It has to be
	initialized with a dictionary. On initialization, a Tokenizer
	object is created with source words of the dictionary. 

	You can create a transliterator by this:

	  mytr = Transliterator({'I':'Je', 'am':'suis', 'free':'libre'})
	
	Now, you can translate a english string to french. Try this,

	  print mytr.convert('I am free')

	You will get:
	  
	  Je suis libre
	"""
	#
	dic = {}
	def __init__(self, dic):
		self.dic = dic
		self.tok = Tokenizer(self.dic.keys())
	
	
	def convert (self, str):
		ipa = []
		errors = []
		num_error = 0
		for t in self.tok.tok(str):
			if t == " ":
				ipa.append(t)	
				errors.append(t)
			elif self.dic.has_key(t):
				ipa.append(self.dic[t])
				errors.append(" "*len(t))
			else:
				ipa.append(t)
				errors.append(t)
				num_error += 1

		if num_error > 0 :
			raise InputError("".join(ipa), "".join(errors))
		else:
			return "".join(ipa)
	

#
# Tokenizer
#
class Tokenizer:
	"""Tokenizer
	tokenizes a input string into a list of words.
	It has to be initialized with a lexion (list of words).

	For example, suppose an object named "mytoker" initialized with
	a lexion which contains words: 'I', 'have', 'a', and 'string'.

	  mytoker = Tokenizer(['I, 'have', 'a', 'string'])

	Now, mytok can tokenzie a string 'Ihaveabook' into a list of
	words. Try this,

	  print mytok.toker('Ihaveabook') 

	You will get:

	  ['I', 'have', 'a', 'book']
	"""

	show_process_option = False 
	lexicon = []

	def __init__(self, lexicon):
		self.lexicon = lexicon	

	def look(self, left, right):
		if right == "":
			return left
		else:
			target = left + right[0];

		if ( self.lexicon.count(target) ):
			self.show_process(left + '>' + right)
			return self.look(target, right[1:])
		else:
			self.show_process(left + '|' + right)
			return left;

	def tok(self, str):
		toks = []

		while len(str) > 1:
			tok = self.look("", str)
			if (tok == "" ):
				toks.append(str[0])
				self.show_process(toks)
				str = str[1:]
			else:
				toks.append(tok)
				self.show_process(toks)
				str = str[len(tok):]
			
		if len(str) == 1 :
			toks.append (str)
			self.show_process(toks)

		return toks
	
	def show_process(self, str):
		if self.show_process_option == True:
			print str
	

#
# Converter
#
class Converter:
	"""Converter
	converts a source text file into target form with rules
	provided by a transliteration table file.

	Converter has to be initialized with a filehandle refering
	to a mapping table file.
	"""

	tr = None;
	dic = None;
	opener = "[";
	closer = "]";
	escape = "\\";

	def __init__ (self, dicfile, mode='default'):

		try :
			self.dic = Dictionary(dicfile, mode=mode)
		except DictionaryError, e:
			sys.stderr.write(
				"DictionaryError: " 
				+ "LINE %d" % e.lineno + " "
				+ e.message
				+ "\n" + e.entry + "\n"
				)
			sys.exit(1)
	
		self.tr = Transliterator(self.dic)
		self.set_delimiters(opener=self.dic.getenv('opener'), 
							closer=self.dic.getenv('closer'))
	
	def set_delimiters(self, opener="[", closer="]", escape="\\"):
		self.opener = opener
		self.closer = closer
		self.escape = escape

	def convert_file(self, infile, outfile):
		for line in infile:
			try:
				output = self.convert(line.strip())
			except InputError, e:
				output = "---ERROR---" + e.expression
			finally:	
				outfile.write(output + "\n")	

	def test_file(self, infile, outfile):
		outfile.write("ALTAI2IPA TEST REPORT\n")
		outfile.write("FILE:   " + infile.name + "\n")
		outfile.write("RULE:   " + self.dic.filename + "\n")
		outfile.write("DATE:   " + datetime.datetime.today().strftime("%Y-%m-%d %I:%M:%S %p") + "\n")
		outfile.write("="*64 + "\n")
		outfile.write("LINENO:  INPUT\n         ERROR\n         OUTPUT\n\n")
		lineno = 0
		for line in infile:
			lineno += 1
			try:
				self.test(line.strip())
			except InputError, e:
				outfile.write(("%6d:  " % lineno) + line.strip() + "\n")
				outfile.write(" "*9 + "".join(e.errors) + "\n")
				outfile.write(" "*9 + e.expression + "\n\n")
	
	def test(self, line):
		try:
			return self.convert(line)
		except InputError, e:
			raise e
	
	def split(self, line):
		splitted = []
		open = self.opener
		o = len(self.opener)
		close = self.closer
		c = len(self.closer)
		
		while line != "" :
			i = line.find(open)
			if i == -1 : 
				splitted.append(line)
				break
			splitted.append(line[0:i])
			splitted.append(line[i:i+o])
			
			line = line[i+o:]
			i = line.find(close)
			splitted.append(line[0:i])
			splitted.append(line[i:i+c])
			
			line = line[i+c:]

		return splitted

	def convert (self, line):
		if self.opener == None or self.closer == None:
			return self.convert_anyway(line)

		status = 'out'
		toks = self.split(line.strip()) 
		trtoks = []
		errors = []
		num_error = 0
		for tok in toks:
			if tok == '':
				pass
			elif tok == self.opener:
				trtoks.append(tok)
				errors.append(" "*len(tok))
				status = 'in';	
			elif tok == self.closer :
				trtoks.append(tok)
				errors.append(" "*len(tok))
				status = 'out';
			else :
				if status == 'in':
					try :
						trtoks.append(self.tr.convert(tok))
						errors.append(" "*len(self.tr.convert(tok)))
					except InputError, e:
						trtoks.append(e.expression)
						errors.append(e.errors)
						num_error += 1
				elif status == 'out':
					trtoks.append(tok)
					errors.append(" "*len(tok))

		if num_error > 0:
			raise InputError("".join(trtoks), "".join(errors))
		else:
			return "".join(trtoks)

	# converts everything and ignores delimters
	def convert_anyway(self, str):
		return self.tr.convert(str.strip())	



################################################################
# altai2ipa
#

#
# stdout encoding
#
class Encode:
    def __init__(self, stdout, enc):
        self.stdout = stdout
        self.encoding = enc
    def write(self, s):
        self.stdout.write(s.encode(self.encoding))


#
# interactive mode
#
def interactive_mode(converter, options):
	sys.stderr.write("altai2ipa " + version + " " + date + "\n")
	sys.stderr.write("Try `help' for more information\n")
	sys.stdout = Encode(sys.stdout, options.out_enc)
	while(sys.stdin):
		sys.stdout.write(">>> ")
		line = sys.stdin.readline().strip()
		if line == 'quit':
			break
		elif line == 'print rules':
			converter.tr.dic.print_dic()
		elif line == '':
			sys.stderr.write("Type `quit' to exit")
		try:
			output = converter.convert_anyway(line)
		except InputError, e:
			output = e.expression

		sys.stdout.write(output + "\n")
	sys.exit(0)

#
# batch process: conversion and test
# 
def batch_process(converter, options, args):
	prefix = options.output_prefix
	postfix = options.output_postfix
	err_postfix = '_err'
	for file in args:
		input = codecs.open(file, encoding=options.in_enc, mode='r')
		(filename, ext) = os.path.splitext(file)
		output = codecs.open(prefix + filename + postfix + ext, encoding=options.out_enc, mode='w')
		converter.convert_file(input, output)
		input.close()
		output.close()
	sys.exit(0)

#
# main
#

def main():
	usage = "usage: %prog [OPTION]... [FILE]..."
	description = "Convert Altaic Phonetic Alphabet to Unicode IPA in FILE(s)"
	formatter = TitledHelpFormatter()
	parser = OptionParser(usage, 
				version=version,
				description=description,
				formatter=formatter)

	parser.add_option("-t", "--test", action="store_true", dest="test",
						help="test mode. check errors and print test report.")
	parser.add_option("-o", "--output", metavar="FILE", 
						dest="output", 
						help="write output to FILE (instead of stdout)")
	parser.add_option("-r", "--rule", metavar="FILE", 
						dest="rule",
						default="altai2ipa.tbl",
						help="read rewriting rules from FILE (%default if not specified)")
	parser.add_option("--file-encoding", metavar='ENC',
						dest="in_enc",
						default="utf8",
						help="encoding of input file (default: %default)")
	parser.add_option("--output-encoding", metavar='ENC',
						dest="out_enc",
						default="utf8",
						help="encoding for output (default: %default)")
	parser.add_option("--rule-encoding", metavar='ENC',
						dest='rule_enc',
						default='utf8',
						help='encoding of rule file (default: %default)')
	parser.add_option("--reverse", 
						action="store_true", dest="reverse",
						help="reverse translation")
	parser.add_option("--prefix", metavar='PREFIX',
						dest='output_prefix',
						default='',
						help="prefix for output files (default: '%default')")
	parser.add_option("--postfix", metavar='POSTFIX',
						dest='output_postfix',
						default='_ipa',
						help="postfix for output files (default: '%default')")
	parser.add_option("-i", action="store_true", dest="interactive",
						help="interactive mode")
	
	(options, args) = parser.parse_args()

	# create Converter object
	rule = codecs.open(options.rule, encoding='utf8', mode='r')
	if options.reverse:
		altai2ipa = Converter(rule, 'reverse')
	else :
		altai2ipa = Converter(rule)
	rule.close()

	# set output
	#   file    if option -o is defined
	#   stdout  if option -o is undef
	if options.output:
		output = codecs.open(options.output, encoding=options.out_enc, mode='w')
	else:
		output = Encode(sys.stdout, options.out_enc)

	# -i: interactive console mode
	if options.interactive :
		interactive_mode(altai2ipa, options)

	# check args
	if len(args) == 0:
		sys.stderr.write("Try `" + sys.argv[0] + " --help' for more information\n")
		sys.exit(0)

	# -t: test mode (error checking)
	if options.test:
		for file in args:
			input = codecs.open(file, encoding=options.in_enc, mode='r')
			altai2ipa.test_file(input, output)
			input.close()
		sys.exit(0)

	# default mode 
	if len(args) == 1:
		input = codecs.open(args[0], encoding=options.in_enc, mode='r')
		altai2ipa.convert_file(input, output)
	else :
		batch_process(altai2ipa, options, args)
		

	
if __name__ == "__main__":
	main()




